/*
 *  Copyright 2005 Blandware (http://www.blandware.com)
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.blandware.atleap.common.util;

import org.apache.oro.text.regex.*;

import java.util.*;

/**
 * <p>Contains some useful utilities to work with HTML.</p>
 * <p><a href="HtmlUtil.java.html"><i>View source</i></a></p>
 *
 * @author Roman Puchkovskiy <a href="mailto:roman.puchkovskiy@blandware.com">
 *         &lt;roman.puchkovskiy@blandware.com&gt;</a>
 * @version $Revision: 1.1 $ $Date: 2006/05/04 17:31:45 $
 */
public class HtmlUtil {

    /**
     * Describes an HTML tag.
     */
    protected static class TagDescription {
        public boolean simple;
        public Set allowedAttributes = null;

        public TagDescription(boolean simple, Set allowedAttributes) {
            this.simple = simple;
            this.allowedAttributes = allowedAttributes;
        }
    }

    /**
     * Makes following: strippes tags, leaving only tags for simple text
     * decoration (B, STRONG, I, EM, U, SPAN with style attribute) and most of
     * tag attributes. When a tag needs to be closed and is not closed, it's
     * stripped too.
     *
     * @param html html-code to process
     * @return processed html
     */
    public static String cutOffHtmlAsForumPost(String html) {
        Map allowedTags = new HashMap();
        Set allowedAttibutes;

        // Tags with no attributes allowed
        allowedAttibutes = new HashSet();

        allowedTags.put("b", new TagDescription(false, allowedAttibutes));
        allowedTags.put("strong", new TagDescription(false, allowedAttibutes));
        allowedTags.put("i", new TagDescription(false, allowedAttibutes));
        allowedTags.put("em", new TagDescription(false, allowedAttibutes));
        allowedTags.put("u", new TagDescription(false, allowedAttibutes));
        allowedTags.put("br", new TagDescription(true, allowedAttibutes));
        allowedTags.put("ul", new TagDescription(false, allowedAttibutes));
        allowedTags.put("ol", new TagDescription(false, allowedAttibutes));
        allowedTags.put("li", new TagDescription(false, allowedAttibutes));

        allowedAttibutes.add("style");
        allowedTags.put("span", new TagDescription(false, allowedAttibutes));

        return normalizeHtml(html, allowedTags, true);
    }

    /**
     * Strips tags which do not belong to a given list; also, attributes that
     * do not belong to similar list are stripped.
     * If stripNonPaired is true, non-paired tags are stripped.
     *
     * @param html text to process
     * @param allowedTags mapping from tag names to TagDescription objects.
     * Tag names should be in lower case.
     * @param stripNonPaired whether to strip non-paired tags
     * @return processed html
     */
    public static String normalizeHtml(String html, Map allowedTags, boolean stripNonPaired) {
        if (html == null) {
            return null;
        }

        StringBuffer result = new StringBuffer();

        PatternCompiler compiler = new Perl5Compiler();
        PatternMatcher tagMatcher = new Perl5Matcher();
        PatternMatcher paramMatcher = new Perl5Matcher();

        String tagRegex = "<(\\/?)([\\w]+)(\\b[^>]*)(\\/?)>";
        Pattern tagPattern = null;
        try {
            tagPattern = compiler.compile(tagRegex);
        } catch (MalformedPatternException e) {
            return null;
        }

        String paramRegex = "(\\b)([\\w]+)(=\\\"[^\"]*\\\")";
        Pattern paramPattern = null;
        try {
            paramPattern = compiler.compile(paramRegex);
        } catch (MalformedPatternException e) {
            return null;
        }

        Stack tagStack = new Stack();

        while (tagMatcher.contains(html, tagPattern)) {
            MatchResult tagMatch = tagMatcher.getMatch();
            int tagBeginOffset = tagMatch.beginOffset(0);
            int tagEndOffset = tagMatch.endOffset(0);

            // append non-html text
            result.append(html.substring(0, tagBeginOffset));

            String slash = tagMatch.group(1);
            boolean closing = "/".equals(slash);
            String tagName = tagMatch.group(2).toLowerCase();
            String tailSlash = tagMatch.group(4);
            boolean isTailSlash = "/".equals(tailSlash);
            TagDescription description = (TagDescription) allowedTags.get(tagName);
            if (description != null) {
                // tag is allowed
                // check maybe it's not paired properly
                boolean pairedBadly = (closing && !description.simple && (tagStack.empty() || !tagName.equals(tagStack.peek()))) || (closing && description.simple);
                boolean badTailSlash = (closing && isTailSlash) || (!description.simple && isTailSlash);
                if ((!pairedBadly || !stripNonPaired) && !badTailSlash) {
                    // tag is paired properly or we just don't care about
                    // pairing

                    // update stack if needed
                    if (stripNonPaired && !description.simple) {
                        if (!closing) {
                            tagStack.push(tagName);
                        } else {
                            tagStack.pop();
                        }
                    }

                    String params = tagMatch.group(3);
                    result.append("<").append(slash).append(tagName);

                    while (paramMatcher.contains(params, paramPattern)) {
                        MatchResult paramMatch = paramMatcher.getMatch();
                        int paramBeginOffset = paramMatch.beginOffset(0);
                        int paramEndOffset = paramMatch.endOffset(0);

                        // append non-html text
                        result.append(params.substring(0, paramBeginOffset));

                        String beforeParamName = paramMatch.group(1);
                        String paramName = paramMatch.group(2);
                        if (description.allowedAttributes.contains(paramName)) {
                            // attribute is allwed, preserve it
                            String equalsAndValue = paramMatch.group(3);
                            result.append(beforeParamName).append(paramName)
                                    .append(equalsAndValue);
                        }

                        params = params.substring(paramEndOffset);
                    }

                    // append the 'tail'
                    result.append(params);
                    // append closing >
                    result.append(tailSlash).append(">");
                }
            }

            html = html.substring(tagEndOffset);
        }

        // append the 'tail'
        result.append(html);

        if (stripNonPaired) {
            while (!tagStack.empty()) {
                result.append("</").append(tagStack.pop()).append(">");
            }
        }

        return result.toString();
    }
}
